# src/analyzer.py
import os
import re
from typing import List, Set, Dict, Tuple, Optional
from .runtime_map import map_pkgmods_to_build_rpms, map_cgolibs_to_build_rpms
import hashlib
import re


# 现有：通过扩展名识别的配置/资源文件（会被复制到镜像）
CONFIG_EXTS = (".conf", ".yaml", ".yml", ".json", ".tmpl", ".tpl", ".env", ".ini")

# 常见外部工具名，用于系统依赖建议
COMMON_TOOLS = {"git", "make", "pkg-config", "clang", "gcc"}


# ---------- 小工具 ----------

def _read_text_safe(path: str, max_bytes: int = 256_000) -> str:
    try:
        with open(path, "rb") as f:
            data = f.read(max_bytes)
        return data.decode("utf-8", errors="ignore")
    except Exception:
        return ""

def _scan_by_ext(go_root: str) -> List[str]:
    """按扩展名扫描配置/资源文件（返回绝对路径，供现有 context.py 使用）"""
    out = []
    for root, _, files in os.walk(go_root):
        for name in files:
            if name.endswith(CONFIG_EXTS):
                out.append(os.path.join(root, name))
    return out

def _is_package_main_with_mainfunc(text: str) -> bool:
    """判断内容是否同时包含 'package main' 与 'func main('（粗略静态判断）"""
    return ("package main" in text) and (re.search(r"\bfunc\s+main\s*\(", text) is not None)

# ---------- 新增：入口验证和优先级功能 ----------

def validate_entry(entry_path: str) -> bool:
    """验证指定路径是否为有效的可执行入口"""
    if not os.path.exists(entry_path):
        return False
    content = _read_text_safe(entry_path)
    return _is_package_main_with_mainfunc(content)

def prioritize_entries(entries: list) -> list:
    """对入口按优先级排序，主要服务入口在前"""
    priority_patterns = [
        (r'^(server|main|app)$', 0),                    # 根目录主服务
        (r'^cmd/(server|main|app|etcd)$', 1),           # cmd下的主服务
        (r'^cmd/[^/]+$', 2),                            # cmd下的其他工具
        (r'^(server|main|app)', 3),                     # 包含主服务名的路径
        (r'^[^/]+$', 4),                                # 其他根目录入口
        (r'^(?!.*(test|example|benchmark))', 5),        # 非测试相关
        (r'^tools/', 6),                                # 工具目录
        (r'.*', 7),                                     # 其他所有（包括测试）
    ]
    
    def get_priority(entry):
        path = entry['out_rel'].lower()
        for pattern, priority in priority_patterns:
            if re.match(pattern, path):
                return priority
        return 999  # 兜底
    
    # 按优先级排序，同优先级内按名称排序
    return sorted(entries, key=lambda e: (get_priority(e), e['out_rel']))

def should_skip_probe(entry_path: str) -> bool:
    """根据入口路径判断是否应该跳过动态探测"""
    skip_patterns = [
        r'test',           # 测试相关
        r'example',        # 示例代码  
        r'benchmark',      # 基准测试
        r'antithesis',     # 反例测试
        r'mock',           # 模拟代码
        r'proto',          # protobuf 工具
        r'dump',           # 转储工具
        r'analysis',       # 分析工具
    ]
    
    path_lower = entry_path.lower()
    return any(pattern in path_lower for pattern in skip_patterns)

# ---------- CGO 扫描 ----------

_CGO_LINE_RE = re.compile(r"(^\s*//\s*#cgo[^\n]*|#cgo[^\n]*)", re.MULTILINE)
_FLAG_RE = re.compile(r"-(I|L|l)\s*([^\s\"']+|\"[^\"]+\"|'[^']+')")
_PKGCFG_RE = re.compile(r"#cgo\s+pkg-config:\s*([^\n]+)")

def _parse_cgo_flags(text: str) -> Tuple[Set[str], Set[str], Set[str], Set[str]]:
    """解析 #cgo 行，返回 (libs, lib_paths, include_paths, pkgcfg_modules)"""
    libs, lpaths, ipaths, pkgmods = set(), set(), set(), set()
    for m in _CGO_LINE_RE.finditer(text):
        line = m.group(0)
        # pkg-config: ...
        for pm in _PKGCFG_RE.findall(line):
            for token in pm.split():
                if token.strip():
                    pkgmods.add(token.strip())
        # -I/-L/-l
        for fm in _FLAG_RE.findall(line):
            flag, val = fm[0], fm[1]
            val = val.strip().strip("\"'")
            if flag == "I":
                ipaths.add(val)
            elif flag == "L":
                lpaths.add(val)
            elif flag == "l":
                libs.add(val)
    return libs, lpaths, ipaths, pkgmods

def _project_uses_cgo(go_root: str) -> Tuple[bool, Set[str], Set[str], Set[str], Set[str]]:
    use_cgo = False
    libs: Set[str] = set()
    lpaths: Set[str] = set()
    ipaths: Set[str] = set()
    pkgmods: Set[str] = set()

    for root, _, files in os.walk(go_root):
        for name in files:
            if not name.endswith(".go"):
                continue
            p = os.path.join(root, name)
            s = _read_text_safe(p)
            if 'import "C"' in s or "#cgo " in s or "//go:build cgo" in s or "// +build cgo" in s:
                use_cgo = True
            l, lp, ip, pm = _parse_cgo_flags(s)
            libs |= l
            lpaths |= lp
            ipaths |= ip
            pkgmods |= pm
    return use_cgo, libs, lpaths, ipaths, pkgmods

def _suggest_sys_packages(cgo_libs: Set[str], tools_detected: Set[str], pkgmods: Set[str]) -> List[str]:
    pkgs: Set[str] = set()

    # cgo 基础工具链（如启用 cgo 或代码里直接用了 gcc）
    if cgo_libs or "gcc" in tools_detected:
        pkgs.update({"gcc", "glibc-devel"})

    # 常见工具
    for t in tools_detected:
        if t in {"git", "make", "clang", "pkg-config"}:
            pkgs.add(t)

    # pkg-config：自身 + 由模块映射到的开发包
    if pkgmods:
        pkgs.add("pkg-config")
        pkgs.update(map(set, [map_pkgmods_to_build_rpms(pkgmods)]).__next__())  # 等价于 pkgs.update(list)

    # 使用 cgo -l 的库名映射到 devel 包（统一从 runtime_map 取）
    pkgs.update(map_cgolibs_to_build_rpms(cgo_libs))

    return sorted(pkgs)


# ---------- 系统依赖（工具）扫描 ----------

_EXEC_CMD_RE = re.compile(r'exec\.Command\(\s*"([^"]+)"')
_GO_GENERATE_RE = re.compile(r'^\s*//\s*go:generate\s+(.+)$', re.MULTILINE)

def _scan_tools(go_root: str) -> Set[str]:
    tools: Set[str] = set()
    for root, _, files in os.walk(go_root):
        for name in files:
            if not name.endswith(".go"):
                # 也顺带扫一眼简单脚本名
                if name.endswith((".sh", ".bash")):
                    s = _read_text_safe(os.path.join(root, name))
                    for t in COMMON_TOOLS:
                        if re.search(rf'\b{re.escape(t)}\b', s):
                            tools.add(t)
                continue
            p = os.path.join(root, name)
            s = _read_text_safe(p)
            # exec.Command("tool", ...)
            for m in _EXEC_CMD_RE.findall(s):
                tools.add(m.strip())
            # go:generate 行里常见工具
            for line in _GO_GENERATE_RE.findall(s):
                for t in COMMON_TOOLS:
                    if re.search(rf'\b{re.escape(t)}\b', line):
                        tools.add(t)
            # 代码里出现 pkg-config 字样
            if "pkg-config" in s:
                tools.add("pkg-config")
    return tools

# ---------- 资源/配置路径扫描 ----------

# 典型 API 的字符串参数
_READFILE_RE = re.compile(r'(?:ioutil|os)\.ReadFile\(\s*"([^"]+)"\s*\)')
_OPEN_RE = re.compile(r'os\.Open\(\s*"([^"]+)"\s*\)')
_HTTPDIR_RE = re.compile(r'http\.Dir\(\s*"([^"]+)"\s*\)')
_TMPL_FILES_RE = re.compile(r'template\.(?:ParseFiles|Must\(template\.ParseFiles\))\(\s*"([^"]+)"')
_TMPL_GLOB_RE = re.compile(r'template\.(?:ParseGlob|Must\(template\.ParseGlob\))\(\s*"([^"]+)"')
_VIPER_SETCONF_RE = re.compile(r'viper\.(?:SetConfigFile|SetConfigName)\(\s*"([^"]+)"\s*\)')
_VIPER_ADDPATH_RE = re.compile(r'viper\.AddConfigPath\(\s*"([^"]+)"\s*\)')
_EMBED_RE = re.compile(r'^\s*//\s*go:embed\s+(.+)$', re.MULTILINE)

def _scan_resource_paths(go_root: str) -> Tuple[Set[str], bool, List[str]]:
    """
    返回：
      - code_paths: 代码中出现的路径/模式（相对/绝对混合，不做存在性校验）
      - uses_embed: 是否使用 go:embed
      - embed_patterns: go:embed 的原始模式列表
    """
    code_paths: Set[str] = set()
    uses_embed = False
    embed_patterns: List[str] = []

    for root, _, files in os.walk(go_root):
        for name in files:
            if not name.endswith(".go"):
                continue
            p = os.path.join(root, name)
            s = _read_text_safe(p)

            for rx in (_READFILE_RE, _OPEN_RE, _HTTPDIR_RE, _TMPL_FILES_RE, _TMPL_GLOB_RE, _VIPER_SETCONF_RE, _VIPER_ADDPATH_RE):
                for m in rx.findall(s):
                    if m.strip():
                        code_paths.add(m.strip())

            # go:embed
            for line in _EMBED_RE.findall(s):
                uses_embed = True
                # 可能有多个模式，用空白分割（支持简单引号去除）
                parts = [x.strip().strip('"').strip("'") for x in line.split()]
                for item in parts:
                    if item:
                        embed_patterns.append(item)
    return code_paths, uses_embed, embed_patterns

# ---------- 入口探测与强校验 ----------

def detect_entry(go_path: str) -> Optional[str]:
    """
    入口探测：优先 cmd/*/main.go；其次根目录 main.go；再退化到全工程任意 package main + func main()
    找不到则返回 None（保持对旧 __main__.py 的兼容）
    """
    go_root = os.path.abspath(go_path)

    # 1) cmd/*/main.go
    cmd_dir = os.path.join(go_root, "cmd")
    if os.path.isdir(cmd_dir):
        for root, _, files in os.walk(cmd_dir):
            for name in files:
                if name == "main.go":
                    p = os.path.join(root, name)
                    s = _read_text_safe(p)
                    if _is_package_main_with_mainfunc(s):
                        return p

    # 2) 根目录 main.go
    root_main = os.path.join(go_root, "main.go")
    if os.path.isfile(root_main):
        s = _read_text_safe(root_main)
        if _is_package_main_with_mainfunc(s):
            return root_main

    # 3) 全工程兜底扫描
    for root, _, files in os.walk(go_root):
        for name in files:
            if not name.endswith(".go"):
                continue
            p = os.path.join(root, name)
            s = _read_text_safe(p)
            if _is_package_main_with_mainfunc(s):
                return p

    return None

def analyze_entry(go_path: str) -> str:
    """
    若未检测到可执行入口，抛出异常，供上层直接提示退出。
    """
    entry = detect_entry(go_path)
    if not entry:
        raise RuntimeError("未检测到可执行入口（package main/func main）。该项目可能是库，无法直接容器化。")
    return entry

# 为兼容可能的调用名
assert_executable_project = analyze_entry

# ---------- 模式相关功能 ----------

def _minimize_packages(packages: list) -> list:
    """轻量模式下最小化包依赖"""
    essential_packages = {
        "ca-certificates",  # SSL证书
        "gcc", "glibc-devel"  # CGO基础（如果需要）
    }
    
    # 只保留必需的包
    minimized = []
    for pkg in packages:
        if pkg in essential_packages:
            minimized.append(pkg)
        elif "devel" in pkg and any(essential in pkg for essential in ["gcc", "glibc"]):
            minimized.append(pkg)
    
    return minimized

def analyze_project_with_mode(entry_file: str, go_path: str, build_mode: str = "standard") -> dict:
    """扩展分析函数，支持模式相关优化"""
    project_info = analyze_project(entry_file, go_path)
    
    # 根据模式调整依赖建议
    if build_mode == "lightweight":
        # 轻量模式：最小化依赖
        project_info["sys_packages_suggested"] = _minimize_packages(
            project_info.get("sys_packages_suggested", [])
        )
        project_info["runtime_sys_packages"] = ["ca-certificates"]
        
    elif build_mode == "debug":
        # 调试模式：添加调试工具
        debug_packages = [
            "strace", "gdb", "curl", "wget", "net-tools", 
            "procps-ng", "vim-minimal", "less"
        ]
        current_packages = project_info.get("runtime_sys_packages", [])
        project_info["runtime_sys_packages"] = list(set(current_packages + debug_packages))
    
    else:
        # 标准模式：保持默认配置
        if "runtime_sys_packages" not in project_info:
            project_info["runtime_sys_packages"] = ["ca-certificates", "tzdata"]
    
    project_info["build_mode_name"] = build_mode
    return project_info

# ---------- 对外主函数：依赖静态分析 ----------

def analyze_project(entry_file: str, go_path: str) -> Dict:
    """综合静态扫描：CGO 依赖 / 系统工具 / 资源与配置文件"""
    go_path_abs = os.path.abspath(go_path)
    entry_abs = os.path.abspath(entry_file)
    entry_rel = os.path.relpath(entry_abs, go_path_abs)

    # 提取 go.mod 中的 Go 版本
    go_mod_path = os.path.join(go_path_abs, "go.mod")
    go_version = extract_go_version(go_mod_path)

    # 1) CGO
    use_cgo, cgo_libs, cgo_lib_paths, cgo_include_paths, pkgmods = _project_uses_cgo(go_path_abs)

    # 2) 系统工具
    tools_detected = _scan_tools(go_path_abs)

    # 3) 资源/配置路径
    code_paths, uses_embed, embed_patterns = _scan_resource_paths(go_path_abs)

    # 4) 通过扩展名收集（保持 context.py 的接口：绝对路径）
    configs_by_ext = _scan_by_ext(go_path_abs)

    # 5) 系统包建议（基于 cgo_libs + tools + pkgmods）
    sys_packages_suggested = _suggest_sys_packages(cgo_libs, tools_detected, pkgmods)

    info = {
        # 现有字段（不变）
        "entry": entry_abs,
        "entry_rel": entry_rel,
        "go_path": go_path_abs,
        "use_cgo": use_cgo,
        "configs": configs_by_ext,

        # Go 版本信息
        "go_version": go_version,  # 添加 Go 版本信息

        # CGO 细节
        "cgo_libs": sorted(cgo_libs),
        "cgo_lib_paths": sorted(cgo_lib_paths),
        "cgo_include_paths": sorted(cgo_include_paths),
        "cgo_pkg_config_modules": sorted(pkgmods),

        # 系统工具/包建议
        "tools_detected": sorted(tools_detected),
        "sys_packages_suggested": sys_packages_suggested,

        # 资源/配置路径（代码层面）
        "resource_paths": sorted(code_paths),        # 原样路径/模式（相对/绝对混合）
        "uses_embed": uses_embed,
        "embed_patterns": embed_patterns,
    }
    return info

# ===== 运行期依赖动态分析（ldd）解析 =====

# 常见 .so 到 OpenCloudOS 运行库包名的静态映射（可逐步扩充）
SO_TO_RPM_RUNTIME = {
    "libssl.so.3": "openssl",
    "libcrypto.so.3": "openssl",
    "libz.so.1": "zlib",
    "libsqlite3.so.0": "sqlite",
    "libcurl.so.4": "curl",
    "libpq.so.5": "postgresql-libs",
    "libstdc++.so.6": "libstdc++",
    "libgcc_s.so.1": "libgcc",
    "libm.so.6": "glibc",
    "libpthread.so.0": "glibc",
    "libdl.so.2": "glibc",
    "librt.so.1": "glibc",
    "libc.so.6": "glibc",
}

def parse_ldd_text(ldd_text: str) -> List[str]:
    """从 ldd 文本提取依赖的 .so 名称列表（去重）"""
    sos = set()
    if not ldd_text:
        return []
    for line in ldd_text.splitlines():
        line = line.strip()
        # 形如：libssl.so.3 => /usr/lib64/libssl.so.3 (0x...)
        # 或：linux-vdso.so.1 (0x00007ff...)
        if not line or "statically linked" in line:
            continue
        tok = line.split()
        if not tok:
            continue
        # 第一段一般是 so 名
        name = tok[0]
        if name.endswith(".so") or ".so." in name or name.startswith("lib"):
            sos.add(name)
    return sorted(sos)

def map_sos_to_rpms(so_names: List[str]) -> List[str]:
    """把 .so 名映射为建议安装的运行库包（简单静态映射 + 常驻建议）"""
    pkgs = set()
    for so in so_names:
        if so in SO_TO_RPM_RUNTIME:
            pkgs.add(SO_TO_RPM_RUNTIME[so])
        # 模糊匹配（降级策略）
        elif "ssl" in so or "crypto" in so:
            pkgs.add("openssl")
        elif "sqlite" in so:
            pkgs.add("sqlite")
        elif "curl" in so:
            pkgs.add("curl")
        elif so.startswith("libstdc++"):
            pkgs.add("libstdc++")
        elif so.startswith("libgcc"):
            pkgs.add("libgcc")
        elif so.startswith("libz"):
            pkgs.add("zlib")
        elif so.startswith("libpq"):
            pkgs.add("postgresql-libs")
        elif so.startswith("libc.") or so.startswith("libm.") or so.startswith("libpthread.") or so.startswith("libdl.") or so.startswith("librt."):
            pkgs.add("glibc")

    # 通常 HTTPS/证书链需要
    pkgs.add("ca-certificates")
    return sorted(pkgs)

def detect_all_entries(go_path: str):
    """
    返回所有可执行入口，保留原目录层级作为 out_rel，并给出 bin_name：
    [
      {
        "name": "<用于展示/探针的简名>",
        "abs":  ".../main.go",
        "rel":  "cmd/caddy/main.go",
        "out_rel": "cmd/caddy",      # 只表示目录层级
        "bin_name": "caddy"          # 真正的可执行名（规则见下）
      },
      ...
    ]

    规则：
      - 若入口文件名为 main.go：bin_name = 其上级目录名（根目录则为 app）
      - 否则：bin_name = 文件名去掉 .go
      - name 用于显示/探针，默认取 bin_name
      - 如同一目录(out_rel)下 bin_name 冲突，则对 bin_name 追加短哈希
    """
    go_root = os.path.abspath(go_path)
    entries = []
    seen_rel_dirs = set()

    for root, _, files in os.walk(go_root):
        if "main.go" in files:
            abs_p = os.path.join(root, "main.go")
            rel_p = os.path.relpath(abs_p, go_root)
            dir_rel = os.path.dirname(rel_p)  # 例如 "cmd/caddy" 或 ""
            if dir_rel in ("", "."):
                out_rel = "app"
                bin_name = "app"
            else:
                out_rel = dir_rel.replace("\\", "/")
                bin_name = _clean_name(os.path.basename(out_rel))

            entries.append({
                "name": bin_name,
                "abs": abs_p,
                "rel": rel_p.replace("\\", "/"),
                "out_rel": out_rel,
                "bin_name": bin_name,
            })
            seen_rel_dirs.add(out_rel)

        # 可选：如果你也想支持非 main.go 的入口（例如 tools/foo.go 也带 main 函数），
        # 可在这里继续遍历 files，找出 package main + func main 且文件名 != main.go 的情况，
        # 并设置 bin_name = 去掉 .go 的文件名，out_rel = 该文件所在目录。
        # 当前你的项目以 main.go 为主，先不展开。

    # 兜底：根 main.go
    if not entries:
        root_main = os.path.join(go_root, "main.go")
        if os.path.isfile(root_main):
            entries.append({
                "name": "app",
                "abs": root_main,
                "rel": "main.go",
                "out_rel": "app",
                "bin_name": "app",
            })

    # (out_rel, bin_name) 唯一化
    _derive_unique_bin_names(entries)

    return entries

def extract_go_version(go_mod_path: str) -> str:
    go_version = None
    try:
        with open(go_mod_path, "r") as f:
            for line in f:
                if line.startswith("go "):  # go mod 中的 Go 版本通常以 "go " 开头
                    go_version = line.strip().split()[1]
                    break
    except FileNotFoundError:
        pass  # 如果找不到 go.mod 文件，返回 None
    return go_version if go_version else "未指定 Go 版本"


def _clean_name(s: str) -> str:
    s = re.sub(r'[^A-Za-z0-9_.-]+', '-', s)
    s = re.sub(r'-{2,}', '-', s)
    return s.strip('-') or 'app'

def _short_hash(s: str, n: int = 6) -> str:
    return hashlib.sha1(s.encode('utf-8')).hexdigest()[:n]

def _derive_unique_bin_names(entries):
    """
    确保 (out_rel, bin_name) 组合唯一。如同一 out_rel 下出现同名可执行，则对 bin_name 追加短哈希。
    entries: [{'out_rel': 'cmd/caddy', 'bin_name': 'caddy', ...}, ...]
    """
    used = set()
    for e in entries:
        key = (e['out_rel'], e['bin_name'])
        if key in used:
            base = e['bin_name']
            proposal = f"{base}-{_short_hash(e['rel'])}"
            while (e['out_rel'], proposal) in used:
                proposal = f"{base}-{_short_hash(e['rel']+proposal)}"
            e['bin_name'] = proposal
            key = (e['out_rel'], e['bin_name'])
        used.add(key)